<?php

declare(strict_types=1);

namespace Phpben\Imi\Auth;

use Imi\App;
use Imi\Config;
use Imi\JWT\Exception\InvalidTokenException;
use Imi\Log\Log;
use Imi\Util\ClassObject;
use Lcobucci\Clock\FrozenClock;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Validation\Constraint\IdentifiedBy;
use Lcobucci\JWT\Validation\Constraint\IssuedBy;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Constraint\PermittedFor;
use Lcobucci\JWT\Validation\Constraint\RelatedTo;
use Lcobucci\JWT\Validation\Constraint\ValidAt;
use Phpben\Imi\Auth\Contract\TokenContract;
use Imi\JWT\Facade\JWT as ImiJwt;
use Imi\Cache\CacheManager as Cache;
use Psr\SimpleCache\InvalidArgumentException;
use Imi\RequestContext;
use Imi\ConnectionContext;

class Jwt implements TokenContract
{
    // 配置
    protected $config;

    // jwt配置
    protected $jwtConfig;

    // imi request
    protected $request;

    public function __construct($config)
    {
        $this->config = $config;
        $this->jwtConfig = Config::get("@app.beans.JWT.list." . $config['jwt']['name']);
        $this->request = RequestContext::get('request');
    }

    /**
     * 是否登陆
     * @param string|null $token
     * @return bool
     * @throws InvalidArgumentException
     */
    public function isLogin(?string $token = null): bool
    {
        $token = $token ?: $this->getRequestToken();
        if (!$token) {
            return false;
        }
        if (Cache::get($this->config['cache'], $this->getTokenName($token)) == 'err') {
            return false;
        }
        if (ConnectionContext::get($this->getTokenName($token))) {
            return true;
        }
        if (!$this->pasreToken($token)) {
            return false;
        }
        return true;
    }

    /**
     * 退出登陆
     * @param string|null $token
     * @return bool
     */
    public function logout(?string $token = null): bool
    {
        $token = $token ?: $this->getRequestToken();
        if (!$token) {
            return false;
        }
        try {
            Cache::set($this->config['cache'], $this->getTokenName($token), 'err', $this->jwtConfig['expires']);
        } catch (InvalidArgumentException $e) {
            Log::error($e->getCode() . ':' . $e->getMessage());
        }
        ConnectionContext::set($this->getTokenName($token), false);
        return true;
    }

    /**
     * 获取Header中token
     * @return string
     */
    public function getRequestToken(): string
    {
        $token = (string)$this->request->getHeaderLine($this->config['jwt']['header_name']);
        return str_replace($this->config['jwt']['prefix'], '', $token);
    }

    /**
     * 解析Token
     * @param string|null $token
     * @param array $option
     * @return mixed
     * @throws InvalidArgumentException
     * @throws InvalidTokenException
     */
    public function pasreToken(?string $token = null, array $option = [])
    {
        $token = $token ?: $this->getRequestToken();
        if (!$token) {
            return false;
        }
        if (Cache::get($this->config['cache'], $this->getTokenName($token)) == 'err') {
            return false;
        }
        // validate
        try {
            $jwt = App::getBean('JWT');
            $config = $jwt->getConfig($this->config['jwt']['name']);
            $token = $jwt->parseToken($token, $this->config['jwt']['name']);
            $configuration = Configuration::forAsymmetricSigner($config->getSignerInstance(), InMemory::plainText($config->getPrivateKey() ?? ''), InMemory::plainText($config->getPublicKey() ?? ''));
            $constraints = [];
            $config->getId() && $constraints[] = new IdentifiedBy($config->getId());
            $config->getIssuer() && $constraints[] = new IssuedBy($config->getIssuer());
            $config->getAudience() && $constraints[] = new PermittedFor($config->getAudience());
            $config->getSubject() && $constraints[] = new RelatedTo($config->getSubject());
            if (class_exists(LooseValidAt::class))
            {
                $validAtClass = LooseValidAt::class;
            }
            else
            {
                $validAtClass = ValidAt::class;
            }
            $constraints[] = new $validAtClass(new FrozenClock(new \DateTimeImmutable()));
            if (!$configuration->validator()->validate($token, ...$constraints)) {
                return false;
            }
        } catch (\Exception $e) {
            return false;
        }
        return $token->claims()->get($config->getDataName());
    }

    /**
     * 获取Token
     * @param mixed $data 存入token的数据
     * @param array $option 配置选项
     * @return string
     */
    public function getToken($data, array $option = []): string
    {
        $token = ImiJwt::getToken($data, $this->config['jwt']['name']);
        return $token->toString();
    }

    /**
     * 登陆
     * @param string $username 用户名
     * @param string $password 密码
     * @param array $option
     * @return mixed
     */
    public function login(string $username, string $password, array $option = [])
    {
        $model = $this->config['model']['user'];
        $keys = $this->config['settings']['user_keys'];
        $user = $model::find([$keys['username'] => $username]);
        if (!$user) {
            return false;
        }
        $hasher = new $this->config['settings']['hash'];
        foreach ($keys as $k => $v) {
            $option[$v] = $user->{$v} ?? null;
        }
        $checkPassword = $hasher->check($password, $user->{$keys['password']}, $option);
        if (!$checkPassword) {
            return false;
        }
        if (!($user->status ?? true)) {
            return false;
        }
        $token = $this->getToken([
            'id' => $user->id,
            $keys['username'] => $username
        ]);
        // unique
        if ($this->config['unique'] ?? false) {
            $lastToken = $user->{$keys['token']};
            $lastToken && $this->logout($lastToken);
            $user->update([$keys['token'] => $token]);
        }
        // log
        if ($this->config['model']['login_log'] ?? false) {
            $logModel = $this->config['model']['login_log'];
            if (method_exists($logModel, 'write')) {
                $logModel::write([
                    'user_id' => $user->id,
                    'username' => $user->{$keys['username']},
                    'password' => "****",
                    'ip' => ip(),
                ]);
            }
        }
        //
        ConnectionContext::set($this->getTokenName($token), $user);
        $user = $user->toArray();
        $user['token'] = $token;
        return new \Imi\Util\LazyArrayObject($user);
    }

    /**
     * user信息
     * @param string|null $token
     * @return mixed
     * @throws InvalidArgumentException
     * @throws InvalidTokenException
     */
    public function user(?string $token = null)
    {
        $token = $token ?: $this->getRequestToken();
        if (!$token) {
            return false;
        }
        if ($user = ConnectionContext::get($this->getTokenName($token))) {
            return $user;
        }
        return (object)$this->pasreToken($token);
    }

    /**
     * 获取Token名称
     * @param $token
     * @return string
     */
    public function getTokenName($token): string
    {
        return 'authtoken-' . md5($token);
    }

    /**
     * 检查用户组规则
     * @param array|string $rules 需要检查的规则列表
     * @param int|null $user_id 用户ID
     * @param string $relation or判断其中一条 and检查所有规则
     * @return bool
     * @throws InvalidArgumentException
     * @throws InvalidTokenException
     */
    public function check($rules, ?int $user_id = null, string $relation = 'or'): bool
    {
        is_string($rules) && $rules = [$rules];
        !$user_id && $user_id = $this->user()->id ?? null;
        if (!$user_id) {
            return false;
        }
        $auth = Auth::instance($this->config)->setUserId($user_id);
        $rulelist = $auth->getRuleList(true);
        if (in_array('*', $rulelist)) {
            return true;
        }
        $list = [];
        foreach ($rulelist as $rule) {
            if (in_array($rule, $rules)) {
                $list[] = $rule;
            }
        }
        if ('or' == $relation && !empty($list)) {
            return true;
        }
        $diff = array_diff($rules, $list);
        if ('and' == $relation && empty($diff)) {
            return true;
        }
        return false;
    }
}